// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.Macios.Generator.Attributes;
using Microsoft.Macios.Generator.Context;
using Microsoft.Macios.Generator.DataModel;
using Microsoft.Macios.Generator.Extensions;
using Microsoft.Macios.Generator.Formatters;
using Microsoft.Macios.Generator.IO;
using ObjCBindings;
using static Microsoft.Macios.Generator.Emitters.BindingSyntaxFactory;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using Constructor = ObjCBindings.Constructor;
using Property = Microsoft.Macios.Generator.DataModel.Property;
using TypeInfo = Microsoft.Macios.Generator.DataModel.TypeInfo;

namespace Microsoft.Macios.Generator.Emitters;

file class ConstructorParameterComparer : IEqualityComparer<ImmutableArray<TypeInfo>> {
	/// <summary>
	/// Determines equality by requiring the same method name and identical ordered parameter type sequence.
	/// </summary>
	public bool Equals (ImmutableArray<TypeInfo> x, ImmutableArray<TypeInfo> y)
	{
		return x.SequenceEqual (y);
	}

	/// <summary>
	/// Computes a hash code combining the method name and ordered parameter types.
	/// </summary>
	public int GetHashCode (ImmutableArray<TypeInfo> obj)
	{
		var hash = new HashCode ();
		foreach (var t in obj)
			hash.Add (t);
		return hash.ToHashCode ();
	}
}

/// <summary>
/// Emitter for Objective-C classes.
/// </summary>
class ClassEmitter : IClassEmitter {
	/// <inheritdoc />
	public string GetSymbolName (in Binding binding)
	{
		var outerClasses = binding.OuterClasses.Select (x => x.Name);
		var prefix = string.Join ('.', outerClasses);
		return string.IsNullOrEmpty (prefix)
			? binding.Name
			: $"{prefix}.{binding.Name}";
	}

	/// <inheritdoc />
	public IEnumerable<string> UsingStatements => [
		"System",
		"System.Drawing",
		"System.Diagnostics",
		"System.ComponentModel",
		"System.Runtime.Versioning",
		"System.Runtime.InteropServices",
		"System.Diagnostics.CodeAnalysis",
		"ObjCRuntime",
	];

	/// <summary>
	/// Emit the code for all the constructors in the class.
	/// </summary>
	/// <param name="context">The current binding context.</param>
	/// <param name="classBlock">Current class block.</param>
	void EmitConstructors (in BindingContext context, TabbedWriter<StringWriter> classBlock)
	{
		// merge the constructors and the protocol constructors for the current class, use a dict to avoid duplicates
		var allConstructors = new Dictionary<ImmutableArray<TypeInfo>, DataModel.Constructor> (new ConstructorParameterComparer ());
		foreach (var constructor in context.Changes.Constructors) {
			if (constructor.Selector is null)
				continue;
			var key = constructor.Parameters.Select (x => x.Type).ToImmutableArray ();
			allConstructors.TryAdd (key, constructor);
		}

		foreach (var constructor in context.Changes.ProtocolConstructors) {
			if (constructor.Selector is null)
				continue;
			var key = constructor.Parameters.Select (x => x.Type).ToImmutableArray ();
			allConstructors.TryAdd (key, constructor);
		}

		// create the ui thread check to be used in the constructors that come from a protocol factory method
		var uiThreadCheck = (context.NeedsThreadChecks)
			? EnsureUiThread (context.RootContext.CurrentPlatform)
			: null;

		// When dealing with constructors we cannot sort them by name because the name is always the same as the class
		// instead we will sort them by the selector name so that we will always generate the constructors in the same order
		foreach (var constructor in allConstructors.Values.OrderBy (c => c.ExportMethodData.Selector)) {
			classBlock.AppendMemberAvailability (constructor.SymbolAvailability);
			classBlock.AppendGeneratedCodeAttribute (optimizable: true);
			if (constructor.ExportMethodData.Flags.HasFlag (Constructor.DesignatedInitializer)) {
				classBlock.AppendDesignatedInitializer ();
			}

			if (GeneratorConfiguration.BGenCompatible && !constructor.SkipRegistration) {
				classBlock.AppendBgenExportAttribute (constructor.Selector);
			}

			using (var constructorBlock = classBlock.CreateBlock (constructor.ToDeclaration (withBaseNSFlag: true).ToString (), block: true)) {
				if (uiThreadCheck is not null && constructor is { IsProtocolConstructor: true, IsThreadSafe: false }) {
					// if we are dealing with a protocol constructor, we need to ensure we are on the UI thread, this
					// happens for example with NSCoding in ui elements.
					constructorBlock.Write (uiThreadCheck.ToString ());
					constructorBlock.WriteLine ();
				}
				// retrieve the method invocation via the factory, this will generate the necessary arguments
				// transformations and the invocation
				var invocations = GetInvocations (constructor);

				// init the needed temp variables
				foreach (var argument in invocations.Arguments) {
					constructorBlock.Write (argument.Validations, verifyTrivia: false);
					constructorBlock.Write (argument.Initializers, verifyTrivia: false);
					constructorBlock.Write (argument.PreCallConversion, verifyTrivia: false);
				}

				// simply call the send or sendSuper accordingly
				constructorBlock.WriteRaw (
$@"if (IsDirectBinding) {{
	{ExpressionStatement (invocations.Send)}
}} else {{
	{ExpressionStatement (invocations.SendSuper)}
}}
");

				// before we leave the methods, do any post operations
				foreach (var argument in invocations.Arguments) {
					constructorBlock.Write (argument.PostCallConversion, verifyTrivia: false);
				}
			}
			classBlock.WriteLine ();
		}
	}

	/// <summary>
	/// Emit the code for all the notifications in the class.
	/// </summary>
	/// <param name="properties">All properties of the class, the method will filter those that are notifications.</param>
	/// <param name="classBlock">Current class block.</param>
	void EmitNotifications (in ImmutableArray<Property> properties, TabbedWriter<StringWriter> classBlock)
	{
		if (properties.Length == 0)
			return;

		// create a nested static class with the notification helpers
		using (var notificationClass = classBlock.CreateBlock ("public static partial class Notifications", true)) {
			notificationClass.WriteLine ();
			// generate two methods per notification
			foreach (var notification in properties) {
				var count = 12; // == "Notification".Length;
				var name = $"Observe{notification.Name [..^count]}";
				var notificationCenter = notification.ExportFieldData.IsNullOrDefault || notification.ExportFieldData.FieldData.NotificationCenter is null
					? $"{NotificationCenter}.DefaultCenter"
					: notification.ExportFieldData.FieldData.NotificationCenter;
				var eventType = notification.ExportFieldData.IsNullOrDefault || notification.ExportFieldData.FieldData.Type is null
					? NSNotificationEventArgs.ToString ()
					: notification.ExportFieldData.FieldData.Type;
				// use the raw writer which makes it easier to read in this case
				notificationClass.WriteRaw (
@$"public static {NSObject} {name} ({BindingSyntaxFactory.EventHandler}<{eventType}> handler)
{{
	return {notificationCenter}.AddObserver ({notification.Name}, notification => handler (null, new {eventType} (notification)));
}}

public static NSObject {name} ({NSObject} objectToObserve, {BindingSyntaxFactory.EventHandler}<{eventType}> handler)
{{
	return {notificationCenter}.AddObserver ({notification.Name}, notification => handler (null, new {eventType} (notification)), objectToObserve);
}}

"
);
			}
		}
	}

	/// <summary>
	/// Emit the events for the given delegates.
	/// </summary>
	/// <param name="bindingContext">The current binding context.</param>
	/// <param name="delegates">The delegate properties.</param>
	/// <param name="classBlock">Current class block.</param>
	void EmitEvents (in BindingContext bindingContext, in ImmutableArray<Property> delegates, TabbedWriter<StringWriter> classBlock)
	{
		var applicationClass = bindingContext.RootContext.CurrentPlatform == PlatformName.MacOSX
			? $"{NSApplication}" : $"{UIApplication}";
		// loop over the delegates
		foreach (var property in delegates) {
			// see if we have a strong type
			if (property.ExportPropertyData.StrongDelegateType.IsNullOrDefault)
				continue;

			// the following are the properties that will be used for the events to register to the delegate
			var strongDelegateType = property.ExportPropertyData.StrongDelegateType.Name [1..];
			var strongDelegateName = property.ExportPropertyData.StrongDelegateName
									 ?? property.Name [4..]; // remove the 'Weak' prefix
			var internalType =
				Nomenclator.GetInternalDelegateForEventName (property.ExportPropertyData.StrongDelegateType);
			// this method is reused by all elements, so we want to calculate the name only once
			var ensureMethod = $"Ensure{strongDelegateType}";
			using (var getInternalType =
				   classBlock.CreateBlock ($"internal virtual Type GetInternalEvent{strongDelegateName}Type", true)) {
				getInternalType.WriteLine ($"get => typeof ({internalType});");
			}

			classBlock.WriteLine ();
			using (var createInternalType =
				   classBlock.CreateBlock ($"internal virtual {internalType} CreateInternalEvent{strongDelegateName}Type ()", true)) {
				createInternalType.WriteLine ($"return new ();");
			}

			classBlock.WriteLine ();
			using (var ensureInternalType = classBlock.CreateBlock (
					   $"internal {internalType} {ensureMethod} ()", true)) {
				ensureInternalType.WriteRaw (
$@"if ({property.Name} is not null)
	{applicationClass}.EnsureEventAndDelegateAreNotMismatched ({property.Name}, GetInternalEvent{strongDelegateName}Type);
var del = {strongDelegateName} as {internalType};
if (del is null) {{
	del = CreateInternalEvent{strongDelegateName}Type ();
	{strongDelegateName} = ({property.ExportPropertyData.StrongDelegateType.Name})del;
}}
return del;
");
			}

			classBlock.WriteLine ();
			// loop over the events, those should be present in the property for the delegate
			foreach (var eventInfo in property.ExportPropertyData.StrongDelegateType.Events) {
				// create the event args type name
				var eventHandler = eventInfo.EventArgsType is null
					? BindingSyntaxFactory.EventHandler.ToString ()
					: $"{BindingSyntaxFactory.EventHandler}<{eventInfo.EventArgsType}>";
				using (var eventBlock =
					   classBlock.CreateBlock ($"public event {eventHandler} {eventInfo.Name}", true)) {
					eventBlock.WriteLine ($"add {{ {ensureMethod} ()!.{eventInfo.Name.Decapitalize ()} += value; }}");
					eventBlock.WriteLine ($"remove {{ {ensureMethod} ()!.{eventInfo.Name.Decapitalize ()} -= value; }}");
				}
				classBlock.WriteLine ();
			}
		}
	}

	/// <inheritdoc />
	public bool TryEmit (in BindingContext bindingContext, [NotNullWhen (false)] out ImmutableArray<Diagnostic>? diagnostics)
	{
		diagnostics = null;
		if (bindingContext.Changes.BindingType != BindingType.Class) {
			diagnostics = [Diagnostic.Create (
					RgenDiagnostics
						.RBI0000, // An unexpected error occurred while processing '{0}'. Please fill a bug report at https://github.com/dotnet/macios/issues/new.
					null,
					bindingContext.Changes.FullyQualifiedSymbol)];
			return false;
		}

		// namespace declaration
		this.EmitNamespace (bindingContext);

		using (var _ = this.EmitOuterClasses (bindingContext, out var builder)) {

			// register the class only if we are not dealing with a static class
			var bindingData = (BindingTypeData<Class>) bindingContext.Changes.BindingInfo;
			// Registration depends on the class name. If the binding data contains a name, we use that one, else
			// we use the name of the class
			var registrationName = bindingData.Name ?? bindingContext.Changes.Name;

			// append the class availability, this will add the necessary attributes to the class
			bindingContext.Builder.AppendMemberAvailability (bindingContext.Changes.SymbolAvailability);
			if (!bindingContext.Changes.IsStatic) {
				builder.WriteLine ($"[Register (\"{registrationName}\", true)]");
			}

			var modifiers = $"{string.Join (' ', bindingContext.Changes.Modifiers)} ";
			using (var classBlock =
				   builder.CreateBlock (
					   $"{(string.IsNullOrWhiteSpace (modifiers) ? string.Empty : modifiers)}class {bindingContext.Changes.Name}",
					   true)) {
				// emit the fields for the selectors before we register the class or anything
				this.EmitSelectorFields (bindingContext, classBlock);

				if (!bindingContext.Changes.IsStatic) {
					classBlock.AppendGeneratedCodeAttribute (optimizable: true);
					classBlock.WriteLine (
						$"static readonly {NativeHandle} {ClassPtr} = {BindingSyntaxFactory.Class}.GetHandle (\"{registrationName}\");");
					classBlock.WriteLine ();
					classBlock.WriteDocumentation (Documentation.Class.ClassHandle (bindingContext.Changes.Name));
					classBlock.WriteLine ($"public override {NativeHandle} ClassHandle => {ClassPtr};");
					classBlock.WriteLine ();

					this.EmitDefaultNSObjectConstructors (className: bindingContext.Changes.Name,
						classBlock: classBlock,
						disableDefaultCtor: bindingData.Flags.HasFlag (ObjCBindings.Class.DisableDefaultCtor));

					// emit any other constructor that is not the default one
					EmitConstructors (bindingContext, classBlock);
				}

				this.EmitFields (bindingContext.Changes.Name, bindingContext.Changes.Properties, classBlock,
					out var notificationProperties);
				this.EmitProperties (bindingContext, classBlock, out var strongDelegates);
				this.EmitMethods (bindingContext, classBlock);

				// emit the events for the delegates
				EmitEvents (bindingContext, strongDelegates, classBlock);

				// emit the notification helper classes, leave this for the very bottom of the class
				EmitNotifications (notificationProperties, classBlock);
			}

			return true;
		}
	}
}
